package com.xzakota.minesweeper.model;

import com.xzakota.minesweeper.R;
import com.xzakota.minesweeper.contant.ConstantPool;
import com.xzakota.minesweeper.controller.Game;
import com.xzakota.minesweeper.controller.GameResource;
import com.xzakota.minesweeper.model.entity.CellBean;
import com.xzakota.minesweeper.model.entity.LevelBean;
import com.xzakota.minesweeper.util.JSONFileUtils;

import javax.sound.sampled.Clip;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * 游戏模型
 * 
 * @author Xzakota
 */
public class GameModel extends Observable implements ActionListener, PropertyListenable {
    public static final int GAME_STATUS_NOT_READY = -1;
    public static final int GAME_STATUS_READY = 0;
    public static final int GAME_STATUS_BEGIN = 1;
    public static final int GAME_STATUS_PLAYING = 2;
    public static final int GAME_STATUS_LOSE = 99;
    public static final int GAME_STATUS_WIN = 100;
    
    private static final Random RANDOM = new Random();
    private final Timer GAME_TIMER = new Timer(1000, this);
    public final LevelBean CUSTOM_LEVEL = LevelBean.LEVEL_CUSTOM;
    
    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
    
    public LevelBean currentGameLevel;
    public CellBean[][] allCells;
    public int currentMineLeft;
    public int playTime;
    public int gameStatus;
    public boolean isSweepReady;
    private int themeType;
    private boolean questionMarkEnable;
    private boolean soundEnable;
    private int currentGameLevelID;
    private CellBean willSweepCell;

    private boolean hasLayMine = false;

    public GameModel() {
        GAME_TIMER.setInitialDelay(0);

        themeType = JSONFileUtils.getInt(ConstantPool.SETTINGS_GAME_THEME, 0);
        soundEnable = JSONFileUtils.getBoolean(ConstantPool.SETTINGS_GAME_SOUND, false);
        questionMarkEnable = JSONFileUtils.getBoolean(ConstantPool.SETTINGS_GAME_QUESTION_MARK, true);
        currentGameLevelID = JSONFileUtils.getInt(ConstantPool.SETTINGS_GAME_CURRENT_LEVEL_ID, LevelBean.ID_BASIC);
        if (currentGameLevelID > LevelBean.ID_EXPENSE || currentGameLevelID < LevelBean.ID_CUSTOM) {
            currentGameLevelID = LevelBean.ID_BASIC;
        }

        gameStatus = GAME_STATUS_NOT_READY;
    }

    public void markACell(CellBean cell) {
        if (gameStatus == GameModel.GAME_STATUS_READY) {
            gameStatus = GAME_STATUS_BEGIN;

        }
        if (cell.status != CellBean.STATUS_UN_SWEEP) {
            return;
        }

        switch (cell.mark) {
            case CellBean.MARK_NO:
                cell.mark = CellBean.MARK_SURE;
                currentMineLeft--;
                break;
            case CellBean.MARK_SURE:
                if (questionMarkEnable) {
                    cell.mark = CellBean.MARK_MAYBE;
                } else {
                    cell.mark = CellBean.MARK_NO;
                }
                currentMineLeft++;
                break;
            case CellBean.MARK_MAYBE:
                cell.mark = CellBean.MARK_NO;
                break;
        }
        cell.setChanged();
        setChanged();
    }

    public void newGame() {
        if (gameStatus != GameModel.GAME_STATUS_READY) {
            currentGameLevel = switch (currentGameLevelID) {
                case LevelBean.ID_INTERMEDIATE -> LevelBean.LEVEL_INTERMEDIATE;
                case LevelBean.ID_EXPENSE -> LevelBean.LEVEL_EXPENSE;
                case LevelBean.ID_CUSTOM -> {
                    LevelBean customLevel = new LevelBean();
                    customLevel.maxX = JSONFileUtils.getInt(ConstantPool.SETTINGS_CUSTOM_LEVEL_W, LevelBean.LEVEL_BASIC.maxX);
                    customLevel.maxY = JSONFileUtils.getInt(ConstantPool.SETTINGS_CUSTOM_LEVEL_H, LevelBean.LEVEL_BASIC.maxY);
                    customLevel.mineAmount = JSONFileUtils.getInt(ConstantPool.SETTINGS_CUSTOM_LEVEL_M, LevelBean.LEVEL_BASIC.mineAmount);
                    limitCustomLevel(customLevel);

                    yield customLevel;
                }
                default -> LevelBean.LEVEL_BASIC;
            };
            hasLayMine = false;
            GAME_TIMER.stop();
            loadGameLevel(currentGameLevel);
            setChanged();
            gameStatus = GAME_STATUS_READY;
        }
        playSound(GameResource.getSoundResource(R.Sound.GAME_START));
    }

    public void exitGame() {
        GAME_TIMER.stop();
        setChanged();
    }

    public void newGameWithCustomLevel(LevelBean customLevel) {
        limitCustomLevel(customLevel);
        CUSTOM_LEVEL.maxX = customLevel.maxX;
        CUSTOM_LEVEL.maxY = customLevel.maxY;
        CUSTOM_LEVEL.mineAmount = customLevel.mineAmount;
        JSONFileUtils.put(ConstantPool.SETTINGS_CUSTOM_LEVEL_W, customLevel.maxX);
        JSONFileUtils.put(ConstantPool.SETTINGS_CUSTOM_LEVEL_H, customLevel.maxY);
        JSONFileUtils.put(ConstantPool.SETTINGS_CUSTOM_LEVEL_M, customLevel.mineAmount);
        newGameWithGameLevel(CUSTOM_LEVEL);
    }

    private void limitCustomLevel(LevelBean customLevel) {
        if (customLevel.maxX > LevelBean.MAX_X) {
            customLevel.maxX = LevelBean.MAX_X;
        }
        if (customLevel.maxX < LevelBean.MIN_X) {
            customLevel.maxX = LevelBean.MIN_X;
        }
        if (customLevel.maxY > LevelBean.MAX_Y) {
            customLevel.maxY = LevelBean.MAX_Y;
        }
        if (customLevel.maxY < LevelBean.MIN_Y) {
            customLevel.maxY = LevelBean.MIN_Y;
        }

        if (customLevel.mineAmount > LevelBean.MAX_AMOUNT) {
            customLevel.mineAmount = LevelBean.MAX_AMOUNT;
        }
        if (customLevel.mineAmount < LevelBean.MIN_AMOUNT) {
            customLevel.mineAmount = LevelBean.MIN_AMOUNT;
        }
    }

    public void newGameWithGameLevel(LevelBean level) {
        gameStatus = GAME_STATUS_NOT_READY;
        currentGameLevel = level;
        currentGameLevelID = currentGameLevel.id;
        JSONFileUtils.put(ConstantPool.SETTINGS_GAME_CURRENT_LEVEL_ID, currentGameLevel.id, true);
        newGame();
    }

    @Override
    public void setChanged() {
        super.setChanged();
        super.notifyObservers();
    }

    @Override
    public PropertyChangeSupport getPropertyChangeSupport() {
        return pcs;
    }

    public void sweepACell() {
        if (gameStatus == GameModel.GAME_STATUS_BEGIN) {
            GAME_TIMER.restart();
            gameStatus = GameModel.GAME_STATUS_PLAYING;
        }
        isSweepReady = false;
        if (willSweepCell != null) {
            int status = sweepACellAt(willSweepCell);
            if (status == GAME_STATUS_LOSE) {
                gameOver();
            } else if (status == GAME_STATUS_WIN) {
                gameWin();
            } else {
                setChanged();
            }
            willSweepCell = null;
        } else {
            setChanged();
        }

    }

    public void sweepRoundCell() {
        if (gameStatus == GameModel.GAME_STATUS_BEGIN) {
            GAME_TIMER.restart();
            gameStatus = GameModel.GAME_STATUS_PLAYING;
        }
        int status = sweepRoundCellAt(willSweepCell);
        switch (status) {
            case GAME_STATUS_PLAYING:
                isSweepReady = false;
                willSweepCell = null;
                setChanged();
                break;
            case GAME_STATUS_LOSE:
                gameOver();
                break;
            case GAME_STATUS_WIN:
                gameWin();
                break;
        }
    }

    public void willSweepACell(CellBean cell) {
        if (gameStatus == GameModel.GAME_STATUS_READY) {
            gameStatus = GameModel.GAME_STATUS_BEGIN;
        }
        if (willSweepCell != null) {
            willSweepCell.status = CellBean.STATUS_UN_SWEEP;
            willSweepCell.setChanged();
        }

        if (cell.status == CellBean.STATUS_UN_SWEEP && cell.mark != CellBean.MARK_SURE) {
            cell.status = CellBean.STATUS_WILL_SWEEP;
            willSweepCell = cell;
            cell.setChanged();
        } else {
            willSweepCell = null;
        }
        isSweepReady = true;
        setChanged();
    }

    private void cancelWillSweepRoundCell(CellBean cell) {
        CellBean[] rounds = getRoundCellAt(cell);
        for (CellBean rCell : rounds) {
            if (rCell != null) {
                if (rCell.status == CellBean.STATUS_WILL_SWEEP) {
                    rCell.status = CellBean.STATUS_UN_SWEEP;
                    rCell.setChanged();
                }
            }
        }
        if (cell.status == CellBean.STATUS_WILL_SWEEP) {
            cell.status = CellBean.STATUS_UN_SWEEP;
            cell.setChanged();
        }
    }

    public void willSweepRoundCell(CellBean cell) {
        if (gameStatus == GameModel.GAME_STATUS_READY) {
            gameStatus = GameModel.GAME_STATUS_BEGIN;
        }
        if (willSweepCell != null) {
            cancelWillSweepRoundCell(willSweepCell);
        }
        if (cell.status == CellBean.STATUS_UN_SWEEP
                || (cell.status == CellBean.STATUS_SWEPT && cell.roundMineAmount != 0)) {
            CellBean[] rounds = getRoundCellAt(cell);
            for (CellBean rCell : rounds) {
                if (rCell != null && rCell.status == CellBean.STATUS_UN_SWEEP
                        && rCell.mark != CellBean.MARK_SURE) {
                    rCell.status = CellBean.STATUS_WILL_SWEEP;
                    rCell.setChanged();
                }
            }
            if (cell.status == CellBean.STATUS_UN_SWEEP
                    && cell.mark != CellBean.MARK_SURE) {
                cell.status = CellBean.STATUS_WILL_SWEEP;
                cell.setChanged();
            }
        }
        isSweepReady = true;
        willSweepCell = cell;
        setChanged();
    }

    private void gameOver() {
        CellBean[][] allCells = this.allCells;
        CellBean[] rowCells;
        CellBean cell;
        for (int i = allCells.length; i-- > 0; ) {
            rowCells = allCells[i];
            for (int j = rowCells.length; j-- > 0; ) {
                cell = rowCells[j];
                cell.froze = true;
                if (cell.hasMine) {
                    cell.mark = CellBean.MARK_IS_A_MINE;
                } else {
                    if (cell.mark == CellBean.MARK_SURE) {
                        cell.mark = CellBean.MARK_WRONG_MARK;
                    }
                }
                cell.setChanged();
            }
        }
        gameStatus = GameModel.GAME_STATUS_LOSE;
        GAME_TIMER.stop();
        setChanged();
        playSound(GameResource.getSoundResource(R.Sound.GAME_LOSE));
        Game.dispatchEvent("gameOver");
    }

    private void gameWin() {
        CellBean[][] allCells = this.allCells;
        CellBean[] rowCells;
        CellBean cell;
        for (int i = allCells.length; i-- > 0; ) {
            rowCells = allCells[i];
            for (int j = rowCells.length; j-- > 0; ) {
                cell = rowCells[j];
                cell.froze = true;
                if (cell.hasMine) {
                    cell.mark = CellBean.MARK_SURE;
                }
                cell.setChanged();
            }
        }
        gameStatus = GameModel.GAME_STATUS_WIN;
        GAME_TIMER.stop();
        setChanged();
        playSound(GameResource.getSoundResource(R.Sound.GAME_WIN));
        Game.dispatchEvent("gameWin");
    }

    private CellBean getCellAt(int x, int y) {
        LevelBean level = currentGameLevel;
        if (x < 0 || x >= level.maxX || y < 0 || y >= level.maxY) {
            return null;
        }
        return allCells[y][x];
    }

    private CellBean[] getRoundCellAt(CellBean cell) {
        int x = cell.x;
        int y = cell.y;
        CellBean[] roundCells = new CellBean[8];
        roundCells[0] = getCellAt(x, y - 1);
        roundCells[1] = getCellAt(x + 1, y - 1);
        roundCells[2] = getCellAt(x + 1, y);
        roundCells[3] = getCellAt(x + 1, y + 1);
        roundCells[4] = getCellAt(x, y + 1);
        roundCells[5] = getCellAt(x - 1, y + 1);
        roundCells[6] = getCellAt(x - 1, y);
        roundCells[7] = getCellAt(x - 1, y - 1);
        return roundCells;
    }

    private void loadGameLevel(LevelBean gameLevel) {
        CellBean[][] allCells = new CellBean[gameLevel.maxY][gameLevel.maxX];
        for (int y = 0; y < allCells.length; y++) {
            for (int x = 0; x < allCells[y].length; x++) {
                allCells[y][x] = new CellBean(x, y);
            }
        }
        int mineAmount = gameLevel.mineAmount;
        currentGameLevel = gameLevel;
        this.allCells = allCells;
        currentMineLeft = mineAmount;
        playTime = 0;
    }

    /**
     * 布雷核心算法，第一个展开的格子总不会是地雷（有地雷数量限制）
     *
     * @param gameLevel 游戏难度
     * @param firstCell 第一个点击的格子
     */
    private void layMine(LevelBean gameLevel, CellBean firstCell) {
        int row = gameLevel.maxX;
        int col = gameLevel.maxY;
        int mineAmount = gameLevel.mineAmount;
        List<Integer> exportCells = new ArrayList<>();
        exportCells.add(firstCell.y * row + firstCell.x);

        for (CellBean cell : getRoundCellAt(firstCell)) {
            if (cell != null) {
                exportCells.add(cell.y * row + cell.x);
            }
        }

        int area = row * col;

        if (mineAmount > area) {
            mineAmount = area;
        }

        int[] a = new int[area];
        boolean b = mineAmount < area / 2;

        for (int i = 0; i < (b ? area - mineAmount : mineAmount); i++) {
            a[i] = b ? 0 : -1;
        }

        for (int i = b ? area - mineAmount : mineAmount; i < area; i++) {
            a[i] = b ? -1 : 0;
        }

        for (int i : exportCells) {
            if (a[i] == -1) {
                a[i] = 0;
                int idd;
                do {
                    idd = RANDOM.nextInt(i + 1);
                } while (exportCells.contains(idd));
                a[idd] = -1;
            }
        }

        for (int i = b ? area - mineAmount : mineAmount; i < area; i++) {
            int idd;
            do {
                idd = RANDOM.nextInt(i + 1);
            } while (exportCells.contains(idd));

            if (a[idd] != (b ? -1 : 0)) {
                a[idd] = b ? -1 : 0;
                a[i] = b ? 0 : -1;
            }
        }

        for (int y = 0; y < allCells.length; y++) {
            for (int x = 0; x < allCells[y].length; x++) {
                int p = y * row + x;
                if (a[p] == -1) {
                    allCells[y][x].hasMine = true;
                }
            }
        }

        hasLayMine = true;
    }

    private void playSound(Clip clip) {
        if (soundEnable) {
            clip.setFramePosition(0);
            clip.start();
        }
    }

    private int sweepACellAt(CellBean cell) {
        if (!hasLayMine) {
            layMine(currentGameLevel, cell);
        }

        final int SWEPT = CellBean.STATUS_SWEPT;
        try {
            cell.status = SWEPT;
            if (cell.hasMine) {
                return GAME_STATUS_LOSE;
            } else {
                int roundCount = 0;
                CellBean[] roundCells = getRoundCellAt(cell);
                for (CellBean rCell : roundCells) {
                    if (rCell != null && rCell.hasMine) {
                        roundCount++;
                    }
                }
                cell.roundMineAmount = roundCount;
                if (roundCount == 0) {
                    for (CellBean rCell : roundCells) {
                        if (rCell != null && rCell.status != SWEPT) {
                            sweepACellAt(rCell);
                        }
                    }
                }
                CellBean cel;
                for (int i = allCells.length; i-- > 0; ) {
                    for (int j = allCells[i].length; j-- > 0; ) {
                        cel = allCells[i][j];
                        if ((!cel.hasMine) && cel.status != SWEPT) {
                            return GAME_STATUS_PLAYING;
                        }
                    }
                }
                return GAME_STATUS_WIN;
            }
        } finally {
            cell.setChanged();
        }
    }

    private int sweepRoundCellAt(CellBean cell) {
        CellBean[] rounds = getRoundCellAt(cell);
        cancelWillSweepRoundCell(cell);
        if (cell.status == CellBean.STATUS_SWEPT && cell.roundMineAmount != 0) {
            int alreadySure = 0;
            for (CellBean rCell : rounds) {
                if (rCell != null && rCell.mark == CellBean.MARK_SURE) {
                    alreadySure++;
                }
            }
            if (alreadySure == cell.roundMineAmount) {
                for (CellBean rCell : rounds) {
                    if (rCell != null
                            && rCell.status == CellBean.STATUS_UN_SWEEP
                            && rCell.mark != CellBean.MARK_SURE) {
                        int status = sweepACellAt(rCell);
                        if (status != GAME_STATUS_PLAYING) {
                            return status;
                        }
                    }
                }
            }
        }
        return GAME_STATUS_PLAYING;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (playTime == 999) {
            gameOver();
            return;
        }
        // playSound(GameResourceManager.getSoundResource(R.Sound.TIME_TICK));
        playTime++;
        setChanged();
    }

    public int getThemeType() {
        return themeType;
    }

    public void setThemeType(int themeType) {
        this.themeType = themeType;
    }

    public boolean isQuestionMarkEnable() {
        return questionMarkEnable;
    }

    public void setQuestionMarkEnable(boolean questionMarkEnable) {
        this.questionMarkEnable = questionMarkEnable;
        JSONFileUtils.put(ConstantPool.SETTINGS_GAME_QUESTION_MARK, questionMarkEnable, true);
    }

    public boolean isSoundEnable() {
        return soundEnable;
    }

    public void setSoundEnable(boolean soundEnable) {
        this.soundEnable = soundEnable;
        JSONFileUtils.put(ConstantPool.SETTINGS_GAME_SOUND, soundEnable, true);
    }

    public int getCurrentGameLevelID() {
        return currentGameLevelID;
    }
}
